package kernel.stl.slicing.regiondetection;

import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import kernel.stl.LineSegment;
import kernel.stl.LineSegmentList;
import kernel.stl.PointXY;
import kernel.stl.PointXYList;
import kernel.stl.PointXYZ;
import kernel.stl.exception.InvalidPointInsertion;
import kernel.stl.slicing.Slice;

/**
 * @author George
 */
public class Region {

    private ArrayList pointList = new ArrayList();
    private boolean closed = false;

    /**Remove todos os pontos da região.
     */
    public void clear() {
        pointList.clear();
    }
   
    /**Retorna a quantidade de pontos da região.
     * @return Quantidade de pontos da região.
     */
    public int getPointCount() {
        return pointList.size();
    }

    /**Quando a região está fechada, ou seja, quando existe um ciclo de pontos,
     * todos os pontos que não estão contidos neste ciclo são removidos.
     * Por exemplo: Suponha uma região formada pelos pontos p0, p1, p2, p3, p4,
     * p5, p0, p7, p8, p9. Observe que é formado um cíclo com os pontos p0, p1, 
     * p2, p3, p4, p5, p0. Neste caso, os pontos p7, p8, p9.
     */
    public RegionList removeUnnecessaryPointsForCycle() {
        int left = -1;
        int right = -1;
        
        RegionList regionList = new RegionList();
        PointXYList pointList = new PointXYList();
        
        for (int i = 0; i < this.getPointCount() - 4; i++) {
            try {
                if (!pointList.hasPoint(this.getPoint(i)))
                    pointList.addPoint(this.getPoint(i));
            } catch (InvalidPointInsertion ex) {
                Logger.getLogger(Region.class.getName()).log(Level.SEVERE, null, ex);
            }
            for (int j = i + 3; j < this.getPointCount(); j++) {                
                try {
                    if (!pointList.hasPoint(this.getPoint(j)))
                    pointList.addPoint(this.getPoint(j));
                } catch (InvalidPointInsertion ex) {
                    Logger.getLogger(Region.class.getName()).log(Level.SEVERE, null, ex);
                }
                if (j >= i + 3 && this.getPoint(i).equals(this.getPoint(j))) {                    
                    Region region = new Region();
                    region.addPointList(pointList);
                    left = i;
                    right = j;
                    pointList = new PointXYList();
                }
            }
        }
        
        if (right == -1 || left == -1)
            return new RegionList();
        
        if (left > 0)
            while (left > 0) {
                this.pointList.remove(0);
                left--;
                right--;
            }
        
        if (right < this.getPointCount() - 1)
            while (right < this.getPointCount() - 1) {
                this.pointList.remove(this.getPointCount() - 1);                
            }
        
        return regionList;
    }
    
    /**Verifica se uma região está fechada.
     * Para que a região esteja fechada é necessário que o primeiro ponto seja
     * igual ao último. Todas as regiões fechadas podem ser preenchidas no
     * momento da plotagem. Além disso, para que uma região seja considerada
     * fechada, são necessários no mínimo 4 pontos. Este é o caso do triângulo
     * que tem 3 pontos. Como o primeiro e último pontos são iguais, então
     * chegamos ao número mínimo de 4.
     * @return True caso a região seja fechada e false no caso contrário.
     */
    public boolean isClosed() {
        return this.closed;
        //if (this.getPointCount() > 3 && this.getFirstPoint().equals(this.getLastPoint()))
        //    return true;
        //return false;
    }
    
    /**Retorna o índice associado a um determinado ponto XY dentro da região.
     * Caso não seja encontrado o ponto, é retornado o índice -1.
     * @param point Ponto a ser localizado na região.
     * @return índice do ponto na região.
     */
    public int indexOf(PointXY point) {
        for (int i = 0; i < this.getPointCount(); i++) {
            if (this.getPoint(i).equals(point)) {
                return i;
            }
        }
        return -1;
    }

    /**Adiciona um novo ponto à região.
     * Este ponto é adicionado logo após o último ponto da região.
     * @param point Ponto a ser adicionado.
     */
    public void addPoint(PointXY point) {
        //if (this.isClosed())
        //    return;
        if (this.getPointCount() >= 3 && this.getFirstPoint().equals(point))
            this.closed = true;
        
        if (this.getPointCount() > 0 && this.getLastPoint().equals(point)) {
            return;
        }
        this.pointList.add(point);
    }

    /**Adiciona um ponto na primeira posição do vetor de pontos da região. 
     * @param point Ponto a ser adicionado.
     */
    public void addPointInFristPosition(PointXY point) {
        if (this.getPointCount() > 0 && this.getFirstPoint().equals(point)) {
            return;
        }
        this.pointList.add(0, point);
    }

    /**Retorna o ponto associado ao índice especificado por parâmetro.
     * @param index índice do ponto a ser retornado.
     * @return ponto associado ao índice.
     */
    public PointXY getPoint(int index) {
        if (index < 0 || index >= this.getPointCount()) {
            throw new IndexOutOfBoundsException();
        }
        return (PointXY) pointList.get(index);
    }

    /**Retorna o primeiro ponto da região.
     * @return primeiro ponto.
     */
    public PointXY getFirstPoint() {
        if (this.pointList.size() == 0) {
            throw new IndexOutOfBoundsException();
        }
        return (PointXY) this.pointList.get(0);
    }

    /**Retorna o último ponto da região.
     * @return último ponto.
     */
    public PointXY getLastPoint() {
        int size = this.pointList.size();
        if (size == 0) {
            throw new IndexOutOfBoundsException();
        }
        return (PointXY) this.pointList.get(size - 1);
    }

    /**Verifica se um ponto está contido na região.
     * @param point ponto a ser localizado na região.
     * @param tolerance tolerância usada para determinar se dois pontos são
     * iguais. Caso a distância entre dois pontos seja menor ou igual a este
     * valor, então eles são considerados iguais.
     * @return
     */
    public boolean hasPoint(PointXY point, double tolerance) {
        for (int i = 0; i < this.getPointCount(); i++) {
            if (this.getPoint(i).equals(point, tolerance)) {
                return true;
            }
        }
        return false;
    }

    /**Verifica se duas regiões são adjacentes. 
     * Para considerar que duas regiões são adjacentes, pelo menos um extremo de
     * uma região deve ser igual ao extremo de outra região.
     * @param other região a ser comparada para determinar se é adjacente ou não.
     * @return True caso as duas regiões sejam adjacentes.
     */
    public boolean isAdjacent(Region other) {
        if (this.getFirstPoint().equals(other.getFirstPoint()) ||
                this.getFirstPoint().equals(other.getLastPoint()) ||
                this.getLastPoint().equals(other.getFirstPoint()) ||
                this.getLastPoint().equals(other.getLastPoint())) {
            return true;
        } else {
            return false;
        }
    }

    /**Conecta duas regiões adjacentes.
     * Caso não sejam adjacentes, nada é feito e nenhuma exceção é lançada. 
     * @param other região a ser conectada.
     * @param tolerance tolerância utilizada para determinar se dois pontos são
     * iguais. A comparação de pontos é necessária para determinar se uma região
     * é adjacente a outra.
     */
    public void connect(Region other, double tolerance) {
        if (this.getFirstPoint().equals(other.getFirstPoint(), tolerance)) {
            for (int i = 1; i < other.getPointCount(); i++) {
                this.addPointInFristPosition(other.getPoint(i));
            }
        } else if (this.getFirstPoint().equals(other.getLastPoint(), tolerance)) {
            for (int i = other.getPointCount() - 1; i > 0; i--) {
                this.addPointInFristPosition(other.getPoint(i));
            }
        } else if (this.getLastPoint().equals(other.getFirstPoint(), tolerance)) {
            for (int i = 1; i < other.getPointCount(); i++) {
                this.addPoint(other.getPoint(i));
            }
        } else if (this.getLastPoint().equals(other.getLastPoint(), tolerance)) {
            for (int i = other.getPointCount() - 2; i > 0; i--) {
                this.addPoint(other.getPoint(i));
            }
        }
    }

    /**Verifica se duas regiões podem ser incorporadas.
     * Este método deve ser chamado antes do método Join.
     * @param other região a ser usada na possível incorporação.
     * @param tolerance Tolerância para determinar se dois pontos são iguais.
     * @return True caso as duas regiões possam ser incorporadas.
     */
    public boolean canJoin(Region other, double tolerance) {
        if (this.hasPoint(other.getFirstPoint(), tolerance)) {
            if (other.getPointCount() == 1) {
                return false;
            }
            int i = this.indexOf(other.getFirstPoint());
            int j = 0;

            int iInc;
            int jInc;

            if (i > 0 && this.getPoint(i - 1).equals(other.getPoint(1))) {
                iInc = -1;
                jInc = 1;
            } else if (i < this.getPointCount() - 1 && this.getPoint(i + 1).equals(other.getPoint(1))) {
                iInc = 1;
                jInc = 1;
            } 
            else if (this.getLastPoint().equals(other.getFirstPoint())) {
                return true;
            }
            else
                return false;

            while (i >= 0 && i < this.getPointCount() && j >= 0 && j < other.getPointCount() &&
                    this.getPoint(i).equals(other.getPoint(j))) {
                i = i + iInc;
                j = j + jInc;
            }
            
            if (j == other.getPointCount() - 1 && j == 0)
                return true;

            if (i < 0) {
                while (j < other.getPointCount()) {
                    this.addPointInFristPosition(other.getPoint(j));
                    j = j + jInc;
                }
            }
            if (i >= this.getPointCount()) {
                while (j < other.getPointCount()) {
                    this.addPoint(other.getPoint(j));
                    j = j + jInc;
                }
            }
        }
        if (this.hasPoint(other.getLastPoint(), tolerance)) {
            if (other.getPointCount() == 1) {
                return false;
            }
            int i = this.indexOf(other.getLastPoint());
            int j = other.getPointCount() - 1 ;

            int iInc = 0;
            int jInc = 0;

            if (i > 0 && this.getPoint(i - 1).equals(other.getPoint(other.getPointCount() - 2))) {
                iInc = -1;
                jInc = -1;
            } 
            else if (i < this.getPointCount() - 1 && this.getPoint(i + 1).equals(other.getPoint(other.getPointCount() - 2))) {
                iInc = 1;
                jInc = -1;
            }
            else if (this.getFirstPoint().equals(other.getLastPoint())) {
                return true;
            }
            else
                return false;
                

            while (i >= 0 && i < this.getPointCount() && j >= 0 && j < other.getPointCount() &&
                    this.getPoint(i).equals(other.getPoint(j))) {
                i = i + iInc;
                j = j + jInc;
            }
            
            if (j == other.getPointCount() - 1 && j == 0)
                return true;

            if (i < 0) {
                if (j < other.getPointCount()) {
                    return true;
                }
            }
            if (i >= this.getPointCount()) {
                if (j < other.getPointCount()) {
                    return true;
                }
            }
        }
        return false;
    }

    public void join(Region other, double tolerance) {
        if (this.hasPoint(other.getFirstPoint(), tolerance)) {
            if (other.getPointCount() == 1) {
                return;
            }
            int i = this.indexOf(other.getFirstPoint());
            int j = 0;

            int iInc;
            int jInc;

            if (i > 0 && this.getPoint(i - 1).equals(other.getPoint(1))) {
                iInc = -1;
                jInc = 1;
            } else if (i < this.getPointCount() - 1 && this.getPoint(i + 1).equals(other.getPoint(1))) {
                iInc = 1;
                jInc = 1;
            } 
            else if (this.getLastPoint().equals(other.getFirstPoint())) {
                iInc = 1;
                jInc = 1;
                i = this.getPointCount() - 1;
                j = 0;
            }
            else
                return;

            while (i >= 0 && i < this.getPointCount() && j >= 0 && j < other.getPointCount() &&
                    this.getPoint(i).equals(other.getPoint(j))) {
                i = i + iInc;
                j = j + jInc;
            }
           
            if (i < 0) {
                while (j < other.getPointCount() && j >= 0) {
                    this.addPointInFristPosition(other.getPoint(j));
                    j = j + jInc;
                }
            }
            if (i >= this.getPointCount()) {
                while (j < other.getPointCount() && j >= 0) {
                    this.addPoint(other.getPoint(j));
                    j = j + jInc;
                }
            }
        }
        else if (this.hasPoint(other.getLastPoint(), tolerance)) {
            if (other.getPointCount() == 1) {
                return;
            }
            int i = this.indexOf(other.getLastPoint());
            int j = other.getPointCount() - 1;

            int iInc = 0;
            int jInc = 0;

            if (i > 0 && this.getPoint(i - 1).equals(other.getPoint(other.getPointCount() - 2))) {
                iInc = -1;
                jInc = -1;
            } else if (i < this.getPointCount() - 1 && this.getPoint(i + 1).equals(other.getPoint(other.getPointCount() - 2))) {
                iInc = 1;
                jInc = -1;
            }
            else if (this.getFirstPoint().equals(other.getLastPoint())) {
               iInc = -1;
               jInc = -1;
               i = 0;
               j = other.getPointCount() - 1;
            }

            while (i >= 0 && i < this.getPointCount() && j >= 0 && j < other.getPointCount() &&
                    this.getPoint(i).equals(other.getPoint(j))) {
                i = i + iInc;
                j = j + jInc;
            }

            if (i < 0) {
                while (j < other.getPointCount() && j >= 0) {
                    this.addPointInFristPosition(other.getPoint(j));
                    j = j + jInc;
                }
            }
            if (i >= this.getPointCount()) {
                while (j < other.getPointCount() && j >= 0) {
                    this.addPoint(other.getPoint(j));
                    j = j + jInc;
                }
            }
        }

    }

    public Slice toSlice() {
        LineSegmentList list = new LineSegmentList();

        for (int i = 0; i < this.getPointCount() - 1; i++) {
            PointXYZ point1 = new PointXYZ(this.getPoint(i).getX(), this.getPoint(i).getY(), 0);
            PointXYZ point2 = new PointXYZ(this.getPoint(i + 1).getX(), this.getPoint(i + 1).getY(), 0);
            LineSegment line = new LineSegment(point1, point2);
            list.addLineSegment(line, 0);
        }
        return new Slice(list);
    }

    private void addPointList(PointXYList pointList) {
        for (int i = 0; i < pointList.count(); i++)
            this.addPoint(pointList.getPoint(i));
    }
}